package co.baiku.boot.tools;

import co.baiku.boot.AjavaerCache;
import co.baiku.boot.cache.service.CacheService;
import co.baiku.boot.common.spring.SpringContext;
import co.baiku.boot.common.tools.JsonTools;
import co.baiku.boot.common.tools.StringTools;
import co.baiku.boot.config.JwtConfig;
import co.baiku.boot.core.orm.dto.AuthDetailDto;
import co.baiku.boot.core.orm.dto.UserDetailDto;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.CompressionCodecs;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.security.core.userdetails.UserDetails;

import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.*;

public class JwtTools {
    public static final JwtTools ME = new JwtTools();

    private JwtTools() {
    }

    private CacheService redis;
    private JwtConfig jwtConfig;

    private static final String CLAIM_KEY_USER_ID = "user_id";
    private static final String CLAIM_KEY_AUTHORITIES = "role";

    public UserDetailDto getUserFromToken(String token) {
        UserDetailDto userDetail;
        try {
            final Claims claims = getClaimsFromToken(token);
            long userId = getUserIdFromToken(token);
            Date expirationDate = getExpirationDateFromToken(token);
            String username = claims.getSubject();
            Object claimsFromDB = claims.get(CLAIM_KEY_AUTHORITIES);
            String claimsJson = JsonTools.beanToJson(claimsFromDB);
            List<AuthDetailDto> roles = JsonTools.jsonToList(claimsJson, AuthDetailDto.class);
            userDetail = new UserDetailDto(userId, username, roles);
            userDetail.setExpire((expirationDate.getTime() - System.currentTimeMillis()) / 1000);
        } catch (Exception e) {
            userDetail = null;
        }
        return userDetail;
    }

    public long getUserIdFromToken(String token) {
        long userId;
        try {
            final Claims claims = getClaimsFromToken(token);
            userId = Long.parseLong(String.valueOf(claims.get(CLAIM_KEY_USER_ID)));
        } catch (Exception e) {
            userId = 0;
        }
        return userId;
    }

    public String getUsernameFromToken(String token) {
        String username;
        try {
            final Claims claims = getClaimsFromToken(token);
            username = claims.getSubject();
        } catch (Exception e) {
            username = null;
        }
        return username;
    }

    public Date getCreatedDateFromToken(String token) {
        Date created;
        try {
            final Claims claims = getClaimsFromToken(token);
            created = claims.getIssuedAt();
        } catch (Exception e) {
            created = null;
        }
        return created;
    }

    public String generateAccessToken(UserDetailDto userDetail) {
        Map<String, Object> claims = generateClaims(userDetail);
        claims.put(CLAIM_KEY_AUTHORITIES, userDetail.getAuthorities());
        return generateAccessToken(userDetail.getUsername(), claims);
    }

    public Date getExpirationDateFromToken(String token) {
        Date expiration;
        try {
            final Claims claims = getClaimsFromToken(token);
            expiration = claims.getExpiration();
        } catch (Exception e) {
            expiration = null;
        }
        return expiration;
    }

    public Boolean canTokenBeRefreshed(String token, LocalDateTime lastPasswordReset) {
        final Date created = getCreatedDateFromToken(token);
        return !isCreatedBeforeLastPasswordReset(created, lastPasswordReset)
                && (!isTokenExpired(token));
    }

    public String refreshToken(String token) {
        String refreshedToken;
        try {
            final Claims claims = getClaimsFromToken(token);
            refreshedToken = generateAccessToken(claims.getSubject(), claims);
        } catch (Exception e) {
            refreshedToken = null;
        }
        return refreshedToken;
    }


    public Boolean validateToken(String token, UserDetails userDetails) {
        UserDetailDto userDetail = (UserDetailDto) userDetails;
        final long userId = getUserIdFromToken(token);
        final String username = getUsernameFromToken(token);
        final Date created = getCreatedDateFromToken(token);
        return (userId == userDetail.getId()
                && username.equals(userDetail.getUsername())
                && !isTokenExpired(token)
                && !isCreatedBeforeLastPasswordReset(created, userDetail.getLastPasswordResetAt())
        );
    }

    public String generateRefreshToken(UserDetailDto userDetail) {
        Map<String, Object> claims = generateClaims(userDetail);
        return generateRefreshToken(userDetail.getUsername(), claims);
    }

    public void putToken(Long userId, String token) {
        redis().put(redisKey(userId), token);
    }

    public void deleteToken(Long userId) {
        redis().delete(redisKey(userId));
    }

    private String redisKey(Long userId) {
        return "AJAVAER:SECURITY:JWT:" + userId;
    }

    public boolean containToken(Long userId, String token) {
        if (userId != null) {
            String tokenDB = redis().get(redisKey(userId));
            if (StringTools.isNotBlank(tokenDB) && tokenDB.equals(token)) {
                return true;
            }
        }
        return false;
    }

    private Claims getClaimsFromToken(String token) {
        Claims claims;
        try {
            claims = Jwts.parser()
                    .setSigningKey(config().getSecret())
                    .parseClaimsJws(token)
                    .getBody();
        } catch (Exception e) {
            claims = null;
        }
        return claims;
    }

    private Date generateExpirationDate(long expiration) {
        return new Date(System.currentTimeMillis() + expiration * 1000);
    }

    private Boolean isTokenExpired(String token) {
        final Date expiration = getExpirationDateFromToken(token);
        return expiration.before(new Date());
    }

    private Boolean isCreatedBeforeLastPasswordReset(Date created, LocalDateTime lastPasswordReset) {
        return (lastPasswordReset != null && created.before(Date.from(lastPasswordReset.atZone(ZoneId.systemDefault()).toInstant())));
    }

    private Map<String, Object> generateClaims(UserDetailDto userDetail) {
        Map<String, Object> claims = new HashMap<>(16);
        claims.put(CLAIM_KEY_USER_ID, userDetail.getId());
        return claims;
    }

    private String generateAccessToken(String subject, Map<String, Object> claims) {
        return generateToken(subject, claims, config().getTokenExpire());
    }

    private String generateRefreshToken(String subject, Map<String, Object> claims) {
        return generateToken(subject, claims, config().getRefreshTokenExpire());
    }


    private String generateToken(String subject, Map<String, Object> claims, long expiration) {
        return Jwts.builder()
                .setClaims(claims)
                .setSubject(subject)
                .setId(UUID.randomUUID().toString().replaceAll("-", ""))
                .setIssuedAt(new Date())
                .setExpiration(generateExpirationDate(expiration))
                .compressWith(CompressionCodecs.DEFLATE)
                .signWith(SignatureAlgorithm.HS256, jwtConfig.getSecret())
                .compact();
    }


    private CacheService redis() {
        if (this.redis == null) {
            this.redis = AjavaerCache.redis();
        }
        return this.redis;
    }

    private JwtConfig config() {
        if (this.jwtConfig == null) {
            this.jwtConfig = SpringContext.getBean(JwtConfig.class);
        }
        return this.jwtConfig;
    }
}
